# coding:utf-8
'''
对数据集进⾏分组并对各组应⽤⼀个函数（⽆论是聚合还是转换），通常是数据分析⼯作中的重要环
节。在将数据集加载、融合、准备好之后，通常就是计算分组统计或⽣成透视表。pandas提供了⼀个
灵活⾼效的gruopby功能，它使你能以⼀种⾃然的⽅式对数据集进⾏切⽚、切块、摘要等操作。
'''
'''
目标：
使⽤⼀个或多个键（形式可以是函数、数组或DataFrame列名）分割pandas对象。
计算分组的概述统计，⽐如数量、平均值或标准差，或是⽤户定义的函数。
应⽤组内转换或其他运算，如规格化、线性回归、排名或选取⼦集等。
计算透视表或交叉表。
执⾏分位数分析以及其它统计分组分析。
'''
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 柯里化一个print
printn = lambda *x: print(*x, sep='\n')

# GroupBy机制--"split-applycombine"（拆分－应⽤－合并）
# 任何分组关键词中的缺失值，都会被从结果中除去
'''
分组键可以有多种形式，且类型不必相同：
    列表或数组，其⻓度与待分组的轴⼀样。
    表示DataFrame某个列名的值。
    字典或Series，给出待分组轴上的值与分组名之间的对应关系。
    函数，⽤于处理轴索引或索引中的各个标签。
最终⽬的仍然是产⽣⼀组⽤于拆分对象的值
'''


def testMechanics():
    df = pd.DataFrame(
        {
            'key1': ['a', 'a', 'b', 'b', 'a'],
            'key2': ['one', 'two', 'one', 'two', 'one'],
            'data1': np.random.randn(5),
            'data2': np.random.randn(5)
        })
    printn('原DF:', df)

    # 假设你想要按key1进⾏分组，并计算data1列的平均值。按列进行分组
    # 实现该功能的⽅式有很多，⽽我们这⾥要⽤的是：访问data1，并根据key1调⽤groupby分组
    grouped = df['data1'].groupby(df['key1'])
    # 变量grouped是⼀个GroupBy对象。它实际上还没有进⾏任何计算，只是含有⼀些有关分组键df['key1']的中间数据⽽已
    printn("分组后的对象:", grouped)

    # 数据（Series）根据分组键进⾏了聚合，产⽣了⼀个新的Series，其索引为key1列中的唯⼀值。之所以结果中索引的名称为key1，是因为原始DataFrame的列df['key1']就叫这个名字
    # 调⽤GroupBy的mean⽅法来计算分组平均值
    s = grouped.mean()
    printn("均值:", s)

    # 如果我们⼀次传⼊多个数组的列表，就会得到不同的结果:即按多列进行分组，传入一个list
    means = df['data1'].groupby([df['key1'], df['key2']]).mean()
    printn("根据多个数组分组的均值结果:", means)
    # 这⾥，我通过两个键对数据进⾏了分组，得到的Series具有⼀个层次化索引（由唯⼀的键对组成）
    s = means.unstack()
    printn("unstack转化为独立索引后:", s)

    # 在这个例⼦中，分组键均为Series。实际上，分组键可以是任何⻓度适当的数组
    states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
    years = np.array([2005, 2005, 2006, 2005, 2006])
    s1 = df['data1'].groupby([states, years]).mean()
    printn("根据任意数组groupby后:", s1)

    # 通常，分组信息就位于相同的要处理DataFrame中。这⾥，你还可以将列名（可以是字符串、数字或其他Python对象）⽤作分组键
    printn("列名分组1:",
           df.groupby('key1').mean())  # df['key2']不是数值数据（俗称“麻烦列”）,结果会过滤掉该列
    printn("列名分组2:", df.groupby(['key1', 'key2']).mean())

    # GroupBy的size⽅法，它可以返回⼀个含有分组⼤⼩的Series
    printn("size方法:", df.groupby(['key1', 'key2']).size())
# testMechanics()


# 对分组进行迭代
# GroupBy对象⽀持迭代，可以产⽣⼀组⼆元元组（由分组名和数据块组成）
def testiter():
    df = pd.DataFrame(
        {
            'key1': ['a', 'a', 'b', 'b', 'a'],
            'key2': ['one', 'two', 'one', 'two', 'one'],
            'data1': np.random.randn(5),
            'data2': np.random.randn(5)
        })
    print("迭代根据key1分组的:")
    for name, group in df.groupby('key1'):
        printn(name, group)
    # 对于多重键的情况，元组的第⼀个元素将会是由键值组成的元组：
    print("迭代根据key1,key2分组的:")
    for (k1, k2), group in df.groupby(['key1', 'key2']):
        print((k1, k2), group)
    # 你可以对这些数据⽚段做任何操作。将这些数据⽚段做成⼀个字典：
    printn("list化:", list(df.groupby('key1')))
    pieces = dict(list(df.groupby('key1')))  # 先转换为元素为二元序列的嵌套序列，然后转换成dict
    printn("做成字典:", pieces['b'])

    # groupby默认是在axis=0上进⾏分组的，通过设置也可以在其他任何轴上进⾏分组。可以根据dtype对列进⾏分组
    grouped = df.groupby(df.dtypes, axis=1)  # axis=1，表示纵向执行，每行操作
    printn("dtypes分组:", grouped)
    print("dtype分组迭代:")
    for dtype, group in grouped:
        printn(dtype, group)
# testiter()


# 选取⼀列或列的⼦集
# 对于由DataFrame产⽣的GroupBy对象，如果⽤⼀个（单个字符串）或⼀组（字符串数组）列名对其进⾏索引，就能实现选取部分列进⾏聚合的⽬的
# df.groupby('key1')['data1']是df['data1'].groupby(df['key1'])语句的语法糖
def testsingleselect():
    df = pd.DataFrame(
        {
            'key1': ['a', 'a', 'b', 'b', 'a'],
            'key2': ['one', 'two', 'one', 'two', 'one'],
            'data1': np.random.randn(5),
            'data2': np.random.randn(5)
        })
    # 对于⼤数据集，很可能只需要对部分列进⾏聚合。
    # 例如，在前⾯那个数据集中，如果只需计算data2列的平均值并以DataFrame形式得到结果，可以这样写
    s = df.groupby(['key1', 'key2'])[['data2']].mean()
    printn('data2的平均值:', s)

    # 这种索引操作所返回的对象是⼀个已分组的DataFrame（如果传⼊的是列表或数组）或已分组的Series（如果传⼊的是标量形式的单个列名）
    s_grouped = df.groupby(['key1', 'key2'])['data2']
    print(s_grouped)
    printn("Series均值:", s_grouped.mean())
# testsingleselect()


# 通过字典或Series进⾏分组,根据映射关系的值分组
def testdictgroup():
    people = pd.DataFrame(
        np.random.randn(5, 5),
        columns=['a', 'b', 'c', 'd', 'e'],
        index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
    people.iloc[2:3, [1, 2]] = np.nan  # Add a few NA values
    printn("设定的DF:", people)

    # 假设已知列的分组关系，并希望根据分组计算列的和
    mapping = {
        'a': 'red',
        'b': 'red',
        'c': 'blue',
        'd': 'blue',
        'e': 'red',
        'f': 'orange'
    }
    # 将这个字典传给groupby，来构造数组，但我们可以直接传递字典
    by_column = people.groupby(mapping, axis=1)  # axis=1 逐行执行，即纵向分组
    printn("展示一下:")
    for i, v in by_column:
        printn(i, v)
    printn("分组和:", by_column.sum())

    # Series也有同样的功能，它可以被看做⼀个固定⼤⼩的映射
    map_series = pd.Series(mapping)
    printn("Series原值:", map_series)
    s2 = people.groupby(map_series, axis=1).count()
    printn("分组统计:", s2)
# testdictgroup()


# 通过函数进⾏分组
'''
⽐起使⽤字典或Series，使⽤Python函数是⼀种更原⽣的⽅法定义分组映射。任何被当做分组键的函
数都会在各个索引值上被调⽤⼀次，其返回值就会被⽤作分组名称
'''


def testFunc():
    people = pd.DataFrame(
        np.random.randn(5, 5),
        columns=['a', 'b', 'c', 'd', 'e'],
        index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
    people.iloc[2:3, [1, 2]] = np.nan  # Add a few NA values
    printn("设定的DF:", people)
    # 计算⼀个字符串⻓度的数组，更简单的⽅法是传⼊len函数
    s = people.groupby(len).sum()  # len函数计算的是index
    printn('根据字符串长度分组计算和:', s)

    # 将函数跟数组、列表、字典、Series混合使⽤也不是问题，因为任何东⻄在内部都会被转换为数组
    key_list = ['one', 'one', 'one', 'two', 'two']
    s1 = people.groupby([len, key_list])
    s2 = s1.min()
    print("展示一下：")
    for i, v in s1:
        printn(i, v)
    printn("混合分组:", s2)
# testFunc()


# 根据索引级别分组
# 层次化索引数据集最⽅便的地⽅就在于它能够根据轴索引的⼀个级别进⾏聚合：
def testindexgroup():
    columns = pd.MultiIndex.from_arrays(
        [['US', 'US', 'US', 'JP', 'JP'], [1, 3, 5, 1, 3]],
        names=['cty', 'tenor'])
    hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
    printn('列名层次化索引:', columns, '列名的层次化索引DF:', hier_df)

    # 要根据级别分组，使⽤level关键字传递级别序号或名字
    s = hier_df.groupby(level='cty', axis=1).count()
    printn("分组统计:", s)
# testindexgroup()


# 数据聚合--agg函数接收自定义的聚合函数，或直接使用内置聚合函数
'''
聚合指的是任何能够从数组产⽣标量值的数据转换过程。之前的例⼦已经⽤过⼀些，⽐如mean、
count、min以及sum等。你可能想知道在GroupBy对象上调⽤mean()时究竟发⽣了什么。许多常⻅的
聚合运算都有进⾏优化
count--分组中非NA值的数量；sum--非NA值的和；mean--非NA值的均值
median--非NA值的算术中位数；std、var--无偏(分母为n-1)标准差和方差
min、max--非NA值的最小和最大值；prod--非NA值的积
first、last--第一个和最后一个非NA值
'''


# quantile可以计算Series或DataFrame列的样本分位数，虽然quantile并没有明确地实现于GroupBy，但它是⼀个Series⽅法，所以这⾥是能⽤的
def testAggregation():
    df = pd.DataFrame(
        {
            'key1': ['a', 'a', 'b', 'b', 'a'],
            'key2': ['one', 'two', 'one', 'two', 'one'],
            'data1': np.random.randn(5),
            'data2': np.random.randn(5)
        })
    grouped = df.groupby('key1')
    printn("quantile方法:", grouped['data1'].quantile(0.9))

    # 如果要使⽤你⾃⼰的聚合函数，只需将其传⼊aggregate或agg⽅法即可
    # ⾃定义聚合函数要⽐那些经过优化的函数慢得多
    def peak_to_peak(arr):
        return arr.max() - arr.min()

    s = grouped.agg(peak_to_peak)
    printn("自定义方法:", s)

    # 有些⽅法（如describe）也是可以⽤在这⾥的，即使严格来讲，它们并⾮聚合运算
    printn("某些方法:", grouped.describe())
# testAggregation()


# ⾯向列的多函数应⽤
'''
对Series或DataFrame列的聚合运算其实就是使⽤aggregate（使⽤⾃定义函数）或调⽤
诸如mean、std之类的⽅法。然⽽，你可能希望对不同的列使⽤不同的聚合函数，或⼀次应⽤多个函
数
'''


def testMultiFunc():
    tips = pd.read_csv('examples/tips.csv')
    # Add tip percentage of total bill
    tips['tip_pct'] = tips['tip'] / tips['total_bill']
    printn('tip百分数实例：', tips[:6])

    # 根据天和smoker对tips进⾏分组
    grouped = tips.groupby(['day', 'smoker'])
    # 将函数名以字符串的形式传入
    grouped_pct = grouped['tip_pct']
    printn("均值:", grouped_pct.agg('mean'))

    # 如果传⼊⼀组函数或函数名，得到的DataFrame的列就会以相应的函数命名
    # 我们传递了⼀组聚合函数进⾏聚合，独⽴对数据分组进⾏评估
    def peak_to_peak(arr):
        return arr.max() - arr.min()

    s = grouped_pct.agg(['mean', 'std', peak_to_peak])
    printn("一组函数聚合后:", s)

    # 如果传⼊的是⼀个由(name,function)元组组成的列表，则各元组的第⼀个元素就会被⽤作DataFrame的列名
    # （可以将这种⼆元元组列表看做⼀个有序映射)
    s1 = grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
    printn("传入(name,function)元组:", s1)

    # 对于DataFrame，你还有更多选择，你可以定义⼀组应⽤于全部列的⼀组函数，或不同的列应⽤不同的函数。
    # 假设我们想要对tip_pct和total_bill列计算三个统计信息
    functions = ['count', 'mean', 'max']
    result = grouped['tip_pct', 'total_bill'].agg(functions)
    printn("DataFrame的一组函数:", result)

    # 结果DataFrame拥有层次化的列，这相当于分别对各列进⾏聚合，然后⽤concat将结果组装到⼀起，使⽤列名⽤作keys参数
    printn("dataframe的层次化效果:", result['tip_pct'])
    # 也可以传⼊带有⾃定义名称的⼀组元组
    ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
    s2 = grouped['tip_pct', 'total_bill'].agg(ftuples)
    printn("传入元组2:", s2)

    # 假设你想要对⼀个列或不同的列应⽤不同的函数。具体的办法是向agg传⼊⼀个从列名映射到函数的字典
    s3 = grouped.agg({'tip': np.max, 'size': 'sum'})
    printn("不同列运用不同函数，传入字典:", s3)
    s4 = grouped.agg({'tip_pct': ['min', 'max', 'mean', 'std'], 'size': 'sum'})
    printn("不同列运用多个不同函数，传入字典2:", s4)

    # 以“没有⾏索引”的形式返回聚合数据
    # 到⽬前为⽌，所有示例中的聚合数据都有由唯⼀的分组键组成的索引（可能还是层次化的）。
    # 由于并不总是需要如此，所以你可以向groupby传⼊as_index=False以禁⽤该功能
    s5 = tips.groupby(['day', 'smoker'], as_index=False).mean()
    printn("无行索引:", s5)
# testMultiFunc()


# apply：⼀般性的“拆分－应⽤－合并”
# apply会将待处理的对象拆分成多个⽚段，然后对各⽚段调⽤传⼊的函数，最后尝试将各⽚段组合到⼀起
def testApply():
    tips = pd.read_csv('examples/tips.csv')
    # Add tip percentage of total bill
    tips['tip_pct'] = tips['tip'] / tips['total_bill']

    # 假设你想要根据分组选出最⾼的5个tip_pct值。⾸先，编写⼀个选取指定列具有最⼤值的⾏的函数：
    def top(df, n=5, column='tip_pct'):
        return df.sort_values(by=column)[-n:]

    printn("最大值的行:", top(tips, n=6))
    '''
    top函数在DataFrame的各个⽚段上调⽤，然后结果由pandas.concat组装到⼀起，
    并以分组名称进⾏了标记。于是，最终结果就有了⼀个层次化索引，其内层索引值来⾃原
    DataFrame
    '''
    # 对smoker分组并⽤该函数调⽤apply，就会得到
    s = tips.groupby('smoker').apply(top)
    printn("分组最大值的行:", s)
    # 传给apply的函数能够接受其他参数或关键字，则可以将这些内容放在函数名后⾯⼀并传⼊
    s1 = tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')
    printn("apply接受其他参数:", s1)
    '''
    当你调⽤诸如describe之类的⽅法时，实际上只是应⽤了下⾯两条代码的快捷⽅式⽽已：
    f = lambda x: x.describe()
    grouped.apply(f)
    '''

    # 禁止分组键
    # 分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传⼊groupby即可禁⽌该效果
    s2 = tips.groupby('smoker', group_keys=False).apply(top)
    printn("禁止分组键,索引层次不变:", s2)
# testApply()


# 分位数和桶分析
def testBucket():
    frame = pd.DataFrame(
        {'data1': np.random.randn(1000),
         'data2': np.random.randn(1000)})
    quartiles = pd.cut(frame.data1, 4)
    printn("cut结果-前10行:", quartiles[:10])

    # 由cut返回的Categories对象可直接传递到groupby。因此，我们可以像下⾯这样对data2列做⼀些统计计算：
    def get_stats(group):
        return {
            'min': group.min(),
            'max': group.max(),
            'count': group.count(),
            'mean': group.mean()
        }

    grouped = frame.data2.groupby(quartiles)
    s = grouped.apply(get_stats).unstack()
    printn("统计结果:", s)

    # 这些都是⻓度相等的桶。要根据样本分位数得到⼤⼩相等的桶，使⽤qcut即可。传⼊labels=False即可只获取分位数的编号
    # Return quantile numbers
    grouping = pd.qcut(frame.data1, 10, labels=False)
    grouped = frame.data2.groupby(grouping)
    s1 = grouped.apply(get_stats).unstack()
    printn("统计结果2:", s1)
# testBucket()


# 示例：⽤特定于分组的值填充缺失值
def testex1():
    # ⽤平均值去填充NA值
    s = pd.Series(np.random.randn(6))
    s[::2] = np.nan
    printn('有缺失值的原始Series:', s)
    ss1 = s.fillna(s.mean())
    printn("NA使用均值填充:", ss1)

    # 你需要对不同的分组填充不同的值。
    # ⼀种⽅法是将数据分组，并使⽤apply和⼀个能够对各数据块调⽤fillna的函数即可
    states = [
        'Ohio', 'New York', 'Vermont', 'Florida', 'Oregon', 'Nevada',
        'California', 'Idaho'
    ]
    group_key = ['East'] * 4 + ['West'] * 4
    data = pd.Series(np.random.randn(8), index=states)
    # ['East'] * 4产⽣了⼀个列表，包括了['East']中元素的四个拷⻉。将这些列表串联起来,将⼀些值设为缺失
    data[['Vermont', 'Nevada', 'Idaho']] = np.nan
    printn("假设的数据:", data)
    ss2 = data.groupby(group_key).mean()
    printn("分组均值:", ss2)
    # 我们可以⽤分组平均值去填充NA值:
    fill_mean = lambda g: g.fillna(g.mean())
    ss3 = data.groupby(group_key).apply(fill_mean)
    printn("分组均值去填充NA:", ss3)

    # 也可以在代码中预定义各组的填充值。由于分组具有⼀个name属性，所以我们可以拿来⽤⼀下
    fill_values = {'East': 0.5, 'West': -1}
    fill_func = lambda g: g.fillna(fill_values[g.name])
    ss4 = data.groupby(group_key).apply(fill_func)
    printn("预定义各组的值去填充NA:", ss4)
# testex1()


# 示例：随机采样和排列
'''
假设你想要从⼀个⼤数据集中随机抽取（进⾏替换或不替换）样本以进⾏蒙特卡罗模拟（Monte Carlo
simulation）或其他分析⼯作。“抽取”的⽅式有很多，这⾥使⽤的⽅法是对Series使⽤sample⽅法
'''


def testex2():
    # Hearts, Spades, Clubs, Diamonds
    suits = ['H', 'S', 'C', 'D']
    card_val = (list(range(1, 11)) + [10] * 3) * 4
    base_names = ['A'] + list(range(2, 11)) + ['J', 'K', 'Q']
    cards = []
    for suit in ['H', 'S', 'C', 'D']:
        cards.extend(str(num) + suit for num in base_names)

    deck = pd.Series(card_val, index=cards)
    # 现在我有了⼀个⻓度为52的Series，其索引包括牌名，值则是21点或其他游戏中⽤于计分的点数（为了简单起⻅，我当A的点数为1）：
    printn('牌的前13行:', deck[:13])

    # 从整副牌中抽出5张
    def draw(deck, n=5):
        return deck.sample(n)

    s = draw(deck)
    printn("sample抽取结果:", s)

    # 你想要从每种花⾊中随机抽取两张牌。由于花⾊是牌名的最后⼀个字符，所以我们可以据此进⾏分组，并使⽤apply
    get_suit = lambda card: card[-1]  # last letter is suit
    s1 = deck.groupby(get_suit).apply(draw, n=2)
    printn("每种花色抽取2张:", s1)

    # 也可以这样写
    printn("2张:", deck.groupby(get_suit, group_keys=False).apply(draw, n=2))
# testex2()


# 示例：分组加权平均数和相关系数
# 根据groupby的“拆分－应⽤－合并”范式，可以进⾏DataFrame的列与列之间或两个Series之间的运算（⽐如分组加权平均）
def testex3():
    df = pd.DataFrame(
        {
            'category': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b'],
            'data': np.random.randn(8),
            'weights': np.random.rand(8)
        })
    printn('原始DF:', df)

    # 然后可以利⽤category计算分组加权平均数：
    grouped = df.groupby('category')
    get_wavg = lambda g: np.average(g['data'], weights=g['weights'])
    s = grouped.apply(get_wavg)
    printn("分组加权平均数:", s)

    # 考虑⼀个来⾃Yahoo!Finance的数据集，其中含有⼏只股票和标准普尔500指数（符号SPX）的收盘价
    close_px = pd.read_csv(
        'examples/stock_px_2.csv',
        parse_dates=True,
        index_col=0)
    printn('收盘价信息:', close_px.info())
    printn("收盘价后四列:", close_px[-4:])

    # 计算⼀个由⽇收益率（通过百分数变化计算）与SPX之间的年度相关系数组成的DataFrame。
    # 先创建⼀个函数，⽤它计算每列和SPX列的成对相关系数
    spx_corr = lambda x: x.corrwith(x['SPX'])
    # 接下来，我们使⽤pct_change计算close_px的百分⽐变化：
    rets = close_px.pct_change().dropna()
    # ⽤年对百分⽐变化进⾏分组，可以⽤⼀个⼀⾏的函数，从每⾏的标签返回每个datetime标签的year属性
    get_year = lambda x: x.year
    by_year = rets.groupby(get_year)
    printn("年度相关系数:", by_year.apply(spx_corr))

    # 当然，还可以计算列与列之间的相关系数。这⾥，我们计算Apple和Microsoft的年相关系数
    s1 = by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
    printn("每年的Apple列和Microsoft列相关系数:", s1)
# testex3()


# 示例：组级别的线性回归
# 顺着上⼀个例⼦继续，你可以⽤groupby执⾏更为复杂的分组统计分析，只要函数返回的是pandas对象或标量值即可
def testex4():
    close_px = pd.read_csv(
        'examples/stock_px_2.csv',
        parse_dates=True,
        index_col=0)
    rets = close_px.pct_change().dropna()
    get_year = lambda x: x.year
    by_year = rets.groupby(get_year)

    # 可以定义下⾯这个regress函数（利⽤statsmodels计量经济学库）对各数据块执⾏普通最⼩⼆乘法（Ordinary Least Squares，OLS）回归：
    import statsmodels.api as sm

    def regress(data, yvar, xvars):
        Y = data[yvar]
        X = data[xvars]
        X['intercept'] = 1.
        result = sm.OLS(Y, X).fit()
        return result.params

# 现在，为了按年计算AAPL对SPX收益率的线性回归，执⾏

    s = by_year.apply(regress, 'AAPL', ['SPX'])
    printn("按年计算AAPL对SPX收益率的线性回归:", s)
# testex4()


# 透视表和交叉表
'''
透视表（pivot table）是各种电⼦表格程序和其他数据分析软件中⼀种常⻅的数据汇总⼯具。它根据⼀
个或多个键对数据进⾏聚合，并根据⾏和列上的分组键将数据分配到各个矩形区域中。在Python和
pandas中，可以通过本章所介绍的groupby功能以及（能够利⽤层次化索引的）重塑运算制作透视
表。DataFrame有⼀个pivot_table⽅法，此外还有⼀个顶级的pandas.pivot_table函数。除能为
groupby提供便利之外，pivot_table还可以添加分项⼩计，也叫做margins
'''
'''
pivot_table参数：values--待聚合的列的名称。默认聚合所有数值列
index--用于分组的列名或其他分组键，出现在结果透视表的行，同理columns
aggfunc--聚合函数或函数列表。默认mean。可以是任何对groupby有效的函数
fill_value--用于替换结果表中的缺失值
dropna--如果为True，不添加条目都为NA的列
margins--添加行/列小计和总计，默认为False
'''


def testpivotT():
    # 回到⼩费数据集，假设我想要根据day和smoker计算分组平均数（pivot_table的默认聚合类型），并将day和smoker放到⾏上
    tips = pd.read_csv('examples/tips.csv')
    # Add tip percentage of total bill
    tips['tip_pct'] = tips['tip'] / tips['total_bill']
    printn('原DF前5行:', tips.head())
    s = tips.pivot_table(index=['day', 'smoker'])  # 指定列名进行透视,原索引消失
    printn("透视表:", s)

    # 可以⽤groupby直接来做。现在，假设我们只想聚合tip_pct和size，⽽且想根据time进⾏分组。将smoker放到列上，把day放到⾏上
    s1 = tips.pivot_table(
        ['tip_pct', 'size'],
        index=['time', 'day'],
        columns='smoker')
    printn("透视表2:", s1)

    # 对这个表作进⼀步的处理，传⼊margins=True添加分项⼩计。这将会添加标签为All的⾏和列，其值对应于单个等级中所有数据的分组统计
    s2 = tips.pivot_table(
        ['tip_pct', 'size'],
        index=['time', 'day'],
        columns='smoker',
        margins=True)
    # All值为平均数：不单独考虑烟⺠与⾮烟⺠（All列），不单独考虑⾏分组两个级别中的任何单项（All⾏)
    printn("透视表3:", s2)

    # 要使⽤其他的聚合函数，将其传给aggfunc即可。例如，使⽤count或len可以得到有关分组⼤⼩的透视表（计数或频率）：
    s3 = tips.pivot_table(
        'tip_pct',
        index=['time', 'smoker'],
        columns='day',
        aggfunc=len,
        margins=True)
    printn("透视表4:", s3)

    # 如果存在空的组合（也就是NA），你可能会希望设置⼀个fill_value
    s4 = tips.pivot_table(
        'tip_pct',
        index=['time', 'size', 'smoker'],
        columns='day',
        aggfunc='mean',
        fill_value=0)
    printn("透视表5:", s4)
# testpivotT()


# 交叉表
# 交叉表（cross-tabulation，简称crosstab）是⼀种⽤于计算分组频率的特殊透视表。
def testcrossTable():
    from io import StringIO
    data = """\
    Sample  Nationality  Handedness
    1   USA  Right-handed
    2   Japan    Left-handed
    3   USA  Right-handed
    4   Japan    Right-handed
    5   Japan    Left-handed
    6   Japan    Right-handed
    7   USA  Right-handed
    8   USA  Left-handed
    9   Japan    Right-handed
    10  USA  Right-handed"""
    data = pd.read_csv(StringIO(data), sep='\\s+')
    printn('设定的数据:', data)

    # 作为调查分析的⼀部分，我们可能想要根据国籍和⽤⼿习惯对这段数据进⾏统计汇总。虽然可以⽤pivot_table实现该功能，但是pandas.crosstab函数会更⽅便
    s = pd.crosstab(
        data.Nationality, data.Handedness,
        margins=True)  # 指定统计的两个维度
    printn("交叉表:", s)

    # crosstab的前两个参数可以是数组或Series，或是数组列表。就像⼩费数据
    tips = pd.read_csv('examples/tips.csv')
    # Add tip percentage of total bill
    tips['tip_pct'] = tips['tip'] / tips['total_bill']
    s1 = pd.crosstab([tips.time, tips.day],
                     tips.smoker,
                     margins=True)  # 两级行索引，一级列索引
    printn("交叉表2:", s1)


testcrossTable()
